﻿using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.ServiceModel;

namespace N3P.WCF.Proxyless
{
    /// <summary>
    /// Creates the async style contract counterparts for sync style contracts
    /// </summary>
    [DebuggerNonUserCode]
    internal static class AsyncContractCreator
    {
		#region Methods (5) 

		// Private Methods (4) 

        /// <summary>
        /// Applies an the appropriate operation contract attribute to a generated method
        /// </summary>
        /// <param name="method">The method to apply the attribute to</param>
        /// <param name="synchronousMethodName">The synchronous method the method is being generated to correspond to</param>
        /// <param name="contractType">The type of the synchronous contract for the service</param>
        /// <param name="namespace">The namespace the service belongs to</param>
        private static void ApplyOperationContractAttribute(MethodBuilder method, string synchronousMethodName, Type contractType, string @namespace)
        {
            var actionString = @namespace + "/" + contractType.Name + "/" + synchronousMethodName;
            var responseActionString = actionString + "Response";
            var contractAttribute =
                new CustomAttributeBuilder(SharedResources.OperationContractAttributeCtor,
                                           new object[0], new[]
                                                              {
                                                                  SharedResources.OperationContractAsyncPatternProperty,
                                                                  SharedResources.OperationContractActionProperty,
                                                                  SharedResources.OperationContractReplyActionProperty
                                                              },
                                           new object[] {true, actionString, responseActionString});
            method.SetCustomAttribute(contractAttribute);
        }

        /// <summary>
        /// Generates the begin and end methods for a given synchronous operation contract
        /// </summary>
        /// <param name="asyncServiceContract">The type to define the begin end pair in</param>
        /// <param name="operationContract">The synchronous method to generate the begin end pair for</param>
        /// <param name="namespace">The namespace that actions for calls should be made in</param>
        private static void GenerateMethod(TypeBuilder asyncServiceContract, MethodInfo operationContract, string @namespace)
        {
            var contractParams = operationContract.GetParameters().Select(x => x.ParameterType).ToList();
            var contractParamNames = operationContract.GetParameters().Select(x => x.Name).ToList();
            var contractReturnType = operationContract.ReturnType;

            contractParams.Add(SharedResources.AsyncCallbackType);
            contractParams.Add(SharedResources.ObjectType);

            contractParamNames.Add("asyncCallback");
            contractParamNames.Add("asyncState");

            var beginMethod = asyncServiceContract.DefineMethod("Begin" + operationContract.Name,
                                                                operationContract.Attributes, SharedResources.AsyncResultType,
                                                                contractParams.ToArray());
            //beginMethod.SetCustomAttribute(SharedResources.OperationContractAttributeUsage);
            ApplyOperationContractAttribute(beginMethod, operationContract.Name, operationContract.DeclaringType, @namespace);

            for (var i = 0; i < contractParams.Count; i++)
                beginMethod.DefineParameter(i + 1, ParameterAttributes.None, contractParamNames[i]);

            var endMethod = asyncServiceContract.DefineMethod("End" + operationContract.Name,
                                                              operationContract.Attributes, contractReturnType,
                                                              new[] { SharedResources.AsyncResultType });
            endMethod.DefineParameter(1, ParameterAttributes.None, "asyncResult");
        }

        /// <summary>
        /// Gets a configured <see cref="T:System.Reflection.Emit.CustomAttributeBuilder" /> that allows the configuration information for the service
        /// to be located even though the name of the contract is now different
        /// </summary>
        /// <param name="originalContract">The original name of the service contract</param>
        /// <param name="namespace">The namespace for the service</param>
        /// <returns>A configured <see cref="T:System.Reflection.Emit.CustomAttributeBuilder" /></returns>
        private static CustomAttributeBuilder GetServiceContractAttribute(string originalContract, string @namespace)
        {
            return new CustomAttributeBuilder(SharedResources.ServiceContractAttributeContructor, new object[0],
                                              new[]
                                                  {
                                                      SharedResources.ServiceContractAttributeNameProperty,
                                                      SharedResources.ServiceContractAttributeConfigurationNameProperty,
                                                      SharedResources.ServiceContractAttributeNamespaceProperty
                                                  },
                                              new object[]
                                                  {
                                                      originalContract, originalContract,
                                                      @namespace + (@namespace == SharedResources.TempUri ? "/" : "")
                                                  });
        }

        /// <summary>
        /// Validates that the given type is in fact a service contract
        /// </summary>
        /// <param name="serviceContractType">The type to validate</param>
        private static void ValidateType(Type serviceContractType)
        {
            if (!serviceContractType.IsInterface)
                throw new ArgumentException("Type argument must be an interface");

            var serviceContractAttributes = serviceContractType.GetCustomAttributes(SharedResources.ServiceContractAttributeType, true);
            if (serviceContractAttributes.Length == 0)
                throw new ArgumentException("Type argument must be a service contract");
        }
		// Internal Methods (1) 

        /// <summary>
        /// Creates the async service contract for a given synchronous service contract
        /// </summary>
        /// <typeparam name="T">The type of synchronous service contract to create a counterpart for</typeparam>
        /// <returns>The newly crated asynchronous service contract</returns>
        internal static Type Create<T>()
        {
            var serviceContractType = typeof(T);

            ValidateType(serviceContractType);
            
            var asyncServiceContract = SharedResources.Module.DefineType(serviceContractType.FullName + "Async", TypeAttributes.Interface | TypeAttributes.Public | TypeAttributes.Abstract);
            var configurationName =
                ((ServiceContractAttribute)
                 serviceContractType.GetCustomAttributes(SharedResources.ServiceContractAttributeType, false)[0])
                    .ConfigurationName ?? serviceContractType.FullName;

            var @namespace =
                ((ServiceContractAttribute)
                 serviceContractType.GetCustomAttributes(SharedResources.ServiceContractAttributeType, false)[0]).Namespace ?? SharedResources.TempUri;

            asyncServiceContract.SetCustomAttribute(GetServiceContractAttribute(configurationName, @namespace));
            
            var contractMethods = serviceContractType.GetMethods().ToList();
            contractMethods.AddRange(serviceContractType.GetInterfaces().SelectMany(x => x.GetMethods()));

            var operationContracts = contractMethods.Where(x => x.IsDefined(SharedResources.OperationContractAttributeType, true)).ToList();

            foreach (var operationContract in operationContracts)
            {
                GenerateMethod(asyncServiceContract, operationContract, @namespace);
            }

            return asyncServiceContract.CreateType();
        }

		#endregion Methods 
    }
}
